# Copyright (C) 2009 The Written Word, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id$

class AbstractProperty (object):
  def __init__ (self, attrname):
    self._attrname = attrname

  def __get__ (self, property, type):
    msg = "Abstract property `%s.%s' accessed" % (type.__name__,
						  self._attrname)
    raise AttributeError (msg)


class AbstractMethod (object):
  """Defines a class to create abstract methods

  Derived from http://code.activestate.com/recipes/266468/"""

  def __init__ (self, funcname):
    self._funcname = funcname

  def __get__ (self, obj, type):
    """Get callable object

    @returns An instance of AbstractMethodHelper.
    """
    return self.AbstractMethodHelper (self._funcname, type)

  class AbstractMethodHelper (object):
    """Abstract method helper class

    An AbstractMethodHelper instance is a callable object that
    represents an abstract method.
    """
    def __init__ (self, funcname, cls):
      self._funcname = funcname
      self._class = cls

    def __call__ (self, *args, **kwds):
      """Call abstract method

      Raises a TypeError, because abstract methods can not be called.
      """
      msg = "Abstract method `%s.%s' called" % (self._class.__name__,
						self._funcname)
      raise TypeError (msg)


class AbstractMeta (type):
  def __init__ (cls, name, bases, *args, **kwds):
    """Configure a new class

    @param cls: Class object
    @param name: Name of the class
    @param bases: All base classes for cls
    """
    super (AbstractMeta, cls).__init__ (cls, name, bases, *args, **kwds)

    # Detach cls.new () from class AbstractMeta, and make it a method
    # of cls.
    cls.__new__ = staticmethod (cls.new)

    # Find all abstract attributes, and assign them to either
    # cls.__abstractmethods__ or cls.__abstractprops__, so we
    # can report them when an instantiation is attempted.
    abstractmethods = []
    abstractprops = []
    ancestors = list (cls.__mro__)
    ancestors.reverse ()  # Start with __builtin__.object
    for ancestor in ancestors:
      for clsname, clst in ancestor.__dict__.items ():
        if isinstance (clst, AbstractMethod):
          abstractmethods.append (clsname)
	elif isinstance (clst, AbstractProperty):
	  abstractprops.append (clsname)
        else:
          if clsname in abstractmethods:
            abstractmethods.remove (clsname)
	  elif clsname in abstractprops:
	    abstractprops.remove (clsname)

    abstractmethods.sort ()
    setattr (cls, '__abstractmethods__', abstractmethods)

    abstractprops.sort ()
    setattr (cls, '__abstractprops__', abstractprops)

  def new (self, cls, *args, **kwds):
    """Allocator for class cls

    @param self: Class object for which an instance should be created.
    @param cls: Same as self.
    """
    if len (cls.__abstractmethods__) or len (cls.__abstractprops__):
      msg = "Can't instantiate class `%s';" % cls.__name__
      if len (cls.__abstractmethods__):
	msg += "\nUnimplemented abstract methods: " + \
	       ", ".join (cls.__abstractmethods__)
      if len (cls.__abstractprops__):
	msg += "\nUnimplemented abstract properties: " + \
	       ", ".join (cls.__abstractprops__)
      raise NotImplementedError (msg)

    return object.__new__ (self)


def abstract (f):
  """Decorator function for abstract methods"""
  return AbstractMethod (f.__name__)
